/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.rocketmq.broker.subscription;

import com.google.common.collect.ImmutableMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.broker.BrokerController;
import org.apache.rocketmq.broker.BrokerPathConfigHelper;
import org.apache.rocketmq.client.Validators;
import org.apache.rocketmq.common.ConfigManager;
import org.apache.rocketmq.common.MixAll;
import org.apache.rocketmq.common.SubscriptionGroupAttributes;
import org.apache.rocketmq.common.attribute.AttributeUtil;
import org.apache.rocketmq.common.constant.LoggerName;
import org.apache.rocketmq.common.topic.TopicValidator;
import org.apache.rocketmq.logging.org.slf4j.Logger;
import org.apache.rocketmq.logging.org.slf4j.LoggerFactory;
import org.apache.rocketmq.remoting.protocol.DataVersion;
import org.apache.rocketmq.remoting.protocol.RemotingSerializable;
import org.apache.rocketmq.remoting.protocol.subscription.SubscriptionGroupConfig;

public class SubscriptionGroupManager extends ConfigManager {
    protected static final Logger log = LoggerFactory.getLogger(LoggerName.BROKER_LOGGER_NAME);

    /**
     * 消费组订阅表：
     * key是消费组名称，value是消费组订阅配置信息
     */
    protected ConcurrentMap<String, SubscriptionGroupConfig> subscriptionGroupTable =
        new ConcurrentHashMap<>(1024);

    private ConcurrentMap<String, ConcurrentMap<String, Integer>> forbiddenTable =
        new ConcurrentHashMap<>(4);

    /**
     * 数据版本对象：记录消费组订阅配置信息数据的当前版本
     */
    private final DataVersion dataVersion = new DataVersion();
    protected transient BrokerController brokerController;

    public SubscriptionGroupManager() {
        this.init();
    }

    public SubscriptionGroupManager(BrokerController brokerController) {
        this(brokerController, true);
    }

    public SubscriptionGroupManager(BrokerController brokerController, boolean init) {
        this.brokerController = brokerController;
        if (init) {
            init();
        }
    }

    protected void init() {
        {
            SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig();
            subscriptionGroupConfig.setGroupName(MixAll.TOOLS_CONSUMER_GROUP);
            putSubscriptionGroupConfig(subscriptionGroupConfig);
        }

        {
            SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig();
            subscriptionGroupConfig.setGroupName(MixAll.FILTERSRV_CONSUMER_GROUP);
            putSubscriptionGroupConfig(subscriptionGroupConfig);
        }

        {
            SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig();
            subscriptionGroupConfig.setGroupName(MixAll.SELF_TEST_CONSUMER_GROUP);
            putSubscriptionGroupConfig(subscriptionGroupConfig);
        }

        {
            SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig();
            subscriptionGroupConfig.setGroupName(MixAll.ONS_HTTP_PROXY_GROUP);
            subscriptionGroupConfig.setConsumeBroadcastEnable(true);
            putSubscriptionGroupConfig(subscriptionGroupConfig);
        }

        {
            SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig();
            subscriptionGroupConfig.setGroupName(MixAll.CID_ONSAPI_PULL_GROUP);
            subscriptionGroupConfig.setConsumeBroadcastEnable(true);
            putSubscriptionGroupConfig(subscriptionGroupConfig);
        }

        {
            SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig();
            subscriptionGroupConfig.setGroupName(MixAll.CID_ONSAPI_PERMISSION_GROUP);
            subscriptionGroupConfig.setConsumeBroadcastEnable(true);
            putSubscriptionGroupConfig(subscriptionGroupConfig);
        }

        {
            SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig();
            subscriptionGroupConfig.setGroupName(MixAll.CID_ONSAPI_OWNER_GROUP);
            subscriptionGroupConfig.setConsumeBroadcastEnable(true);
            putSubscriptionGroupConfig(subscriptionGroupConfig);
        }

        {
            SubscriptionGroupConfig subscriptionGroupConfig = new SubscriptionGroupConfig();
            subscriptionGroupConfig.setGroupName(MixAll.CID_SYS_RMQ_TRANS);
            subscriptionGroupConfig.setConsumeBroadcastEnable(true);
            putSubscriptionGroupConfig(subscriptionGroupConfig);
        }
    }

    protected SubscriptionGroupConfig putSubscriptionGroupConfig(SubscriptionGroupConfig subscriptionGroupConfig) {
        return this.subscriptionGroupTable.put(subscriptionGroupConfig.getGroupName(), subscriptionGroupConfig);
    }

    /**
     * 将消费组订阅配置信息放到消费组订阅配置表中，并返回旧的值。
     * @param subscriptionGroupConfig
     * @return
     */
    protected SubscriptionGroupConfig putSubscriptionGroupConfigIfAbsent(SubscriptionGroupConfig subscriptionGroupConfig) {
        return this.subscriptionGroupTable.putIfAbsent(subscriptionGroupConfig.getGroupName(), subscriptionGroupConfig);
    }

    protected SubscriptionGroupConfig getSubscriptionGroupConfig(String groupName) {
        return this.subscriptionGroupTable.get(groupName);
    }

    protected SubscriptionGroupConfig removeSubscriptionGroupConfig(String groupName) {
        return this.subscriptionGroupTable.remove(groupName);
    }

    /**
     * 更新消费组订阅配置信息
     * @param config
     */
    public void updateSubscriptionGroupConfig(final SubscriptionGroupConfig config) {
        // 如果消费组订阅配置信息中不包含属性集合，则返回空的HashMap集合，否则返回其属性集合
        Map<String, String> newAttributes = request(config);
        // 获取【当前保存的】指定消费组名称的消费组订阅配置信息的属性集合
        Map<String, String> currentAttributes = current(config.getGroupName());
        // 将现有属性集合和新的属性集合进行增删改查等操作进行合并，最终得到结果属性集合。
        Map<String, String> finalAttributes = AttributeUtil.alterCurrentAttributes(
            this.subscriptionGroupTable.get(config.getGroupName()) == null,
            SubscriptionGroupAttributes.ALL,
            ImmutableMap.copyOf(currentAttributes),
            ImmutableMap.copyOf(newAttributes));

        // 设置属性集合
        config.setAttributes(finalAttributes);

        // 将消费组订阅配置信息放到消费组订阅配置表中，并返回旧的值
        SubscriptionGroupConfig old = putSubscriptionGroupConfig(config);
        if (old != null) {
            log.info("update subscription group config, old: {} new: {}", old, config);
        } else {
            log.info("create new subscription group, {}", config);
        }
        // 获取消息存储状态机版本号
        long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0;
        // 记录消息存储状态机的版本号
        dataVersion.nextVersion(stateMachineVersion);
        // 对消费组订阅配置信息进行持久化
        this.persist();
    }

    public void updateForbidden(String group, String topic, int forbiddenIndex, boolean setOrClear) {
        if (setOrClear) {
            setForbidden(group, topic, forbiddenIndex);
        } else {
            clearForbidden(group, topic, forbiddenIndex);
        }
    }

    /**
     * set the bit value to 1 at the specific index (from 0)
     *
     * @param group
     * @param topic
     * @param forbiddenIndex from 0
     */
    public void setForbidden(String group, String topic, int forbiddenIndex) {
        int topicForbidden = getForbidden(group, topic);
        topicForbidden |= 1 << forbiddenIndex;
        updateForbiddenValue(group, topic, topicForbidden);
    }

    /**
     * clear the bit value to 0 at the specific index (from 0)
     *
     * @param group
     * @param topic
     * @param forbiddenIndex from 0
     */
    public void clearForbidden(String group, String topic, int forbiddenIndex) {
        int topicForbidden = getForbidden(group, topic);
        topicForbidden &= ~(1 << forbiddenIndex);
        updateForbiddenValue(group, topic, topicForbidden);
    }

    public boolean getForbidden(String group, String topic, int forbiddenIndex) {
        int topicForbidden = getForbidden(group, topic);
        int bitForbidden = 1 << forbiddenIndex;
        return (topicForbidden & bitForbidden) == bitForbidden;
    }

    public int getForbidden(String group, String topic) {
        ConcurrentMap<String, Integer> topicForbiddens = this.forbiddenTable.get(group);
        if (topicForbiddens == null) {
            return 0;
        }
        Integer topicForbidden = topicForbiddens.get(topic);
        if (topicForbidden == null || topicForbidden < 0) {
            topicForbidden = 0;
        }
        return topicForbidden;
    }

    private void updateForbiddenValue(String group, String topic, Integer forbidden) {
        if (forbidden == null || forbidden <= 0) {
            this.forbiddenTable.remove(group);
            log.info("clear group forbidden, {}@{} ", group, topic);
            return;
        }

        ConcurrentMap<String, Integer> topicsPermMap = this.forbiddenTable.get(group);
        if (topicsPermMap == null) {
            this.forbiddenTable.putIfAbsent(group, new ConcurrentHashMap<>());
            topicsPermMap = this.forbiddenTable.get(group);
        }
        Integer old = topicsPermMap.put(topic, forbidden);
        if (old != null) {
            log.info("set group forbidden, {}@{} old: {} new: {}", group, topic, old, forbidden);
        } else {
            log.info("set group forbidden, {}@{} old: {} new: {}", group, topic, 0, forbidden);
        }

        long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0;
        dataVersion.nextVersion(stateMachineVersion);

        this.persist();
    }

    public void disableConsume(final String groupName) {
        SubscriptionGroupConfig old = getSubscriptionGroupConfig(groupName);
        if (old != null) {
            old.setConsumeEnable(false);
            long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0;
            dataVersion.nextVersion(stateMachineVersion);
        }
    }

    /**
     * 根据消费组名称查找消费组订阅信息
     * @param group 消费组名称
     * @return
     */
    public SubscriptionGroupConfig findSubscriptionGroupConfig(final String group) {
        // 从消费组订阅表中，根据消费组名称获取消费组的订阅配置信息
        SubscriptionGroupConfig subscriptionGroupConfig = getSubscriptionGroupConfig(group);
        // 如果找不到
        if (null == subscriptionGroupConfig) {
            // 如果需要自动创建订阅组信息，或者消费组是系统消费组
            if (brokerController.getBrokerConfig().isAutoCreateSubscriptionGroup() || MixAll.isSysConsumerGroup(group)) {
                // 对消费组名称进行校验，如果校验不通过，则返回null
                if (group.length() > Validators.CHARACTER_MAX_LENGTH || TopicValidator.isTopicOrGroupIllegal(group)) {
                    return null;
                }
                // 创建消费组订阅信息对象
                subscriptionGroupConfig = new SubscriptionGroupConfig();
                // 设置订阅组名称
                subscriptionGroupConfig.setGroupName(group);
                // 将消费组订阅配置信息放到消费组订阅配置表中
                SubscriptionGroupConfig preConfig = putSubscriptionGroupConfigIfAbsent(subscriptionGroupConfig);
                // 如果返回值是null，表示是新建的消费组订阅配置信息，记录日志
                if (null == preConfig) {
                    log.info("auto create a subscription group, {}", subscriptionGroupConfig.toString());
                }
                // 状态机版本
                long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0;
                // 将状态机当前状态机版本号记录下来
                dataVersion.nextVersion(stateMachineVersion);
                // 数据持久化
                /*
                  对配置信息执行持久化。对于消费组订阅配置信息，过程如下：
                    首先将当前记录的信息编码为json字符串；
                    获取配置文件的名称，默认：${user.home}/store/config/subscriptionGroup.json
                    将json写到该文件中。
                 */
                this.persist();
            }
        }
        // 如果找到了就直接返回
        return subscriptionGroupConfig;
    }

    @Override
    public String encode() {
        return this.encode(false);
    }

    /**
     * 获取消费组订阅配置信息文件所在的位置
     * @return
     */
    @Override
    public String configFilePath() {
        // ${user.home}/store
        // ${user.home}/store/config/subscriptionGroup.json
        return BrokerPathConfigHelper.getSubscriptionGroupPath(this.brokerController.getMessageStoreConfig()
            .getStorePathRootDir());
    }

    @Override
    public void decode(String jsonString) {
        if (jsonString != null) {
            SubscriptionGroupManager obj = RemotingSerializable.fromJson(jsonString, SubscriptionGroupManager.class);
            if (obj != null) {
                this.subscriptionGroupTable.putAll(obj.subscriptionGroupTable);
                if (obj.forbiddenTable != null) {
                    this.forbiddenTable.putAll(obj.forbiddenTable);
                }
                this.dataVersion.assignNewOne(obj.dataVersion);
                this.printLoadDataWhenFirstBoot(obj);
            }
        }
    }

    @Override
    public String encode(final boolean prettyFormat) {
        return RemotingSerializable.toJson(this, prettyFormat);
    }

    private void printLoadDataWhenFirstBoot(final SubscriptionGroupManager sgm) {
        Iterator<Entry<String, SubscriptionGroupConfig>> it = sgm.getSubscriptionGroupTable().entrySet().iterator();
        while (it.hasNext()) {
            Entry<String, SubscriptionGroupConfig> next = it.next();
            log.info("load exist subscription group, {}", next.getValue().toString());
        }
    }

    public ConcurrentMap<String, SubscriptionGroupConfig> getSubscriptionGroupTable() {
        return subscriptionGroupTable;
    }

    public ConcurrentMap<String, ConcurrentMap<String, Integer>> getForbiddenTable() {
        return forbiddenTable;
    }

    public void setForbiddenTable(
        ConcurrentMap<String, ConcurrentMap<String, Integer>> forbiddenTable) {
        this.forbiddenTable = forbiddenTable;
    }

    public DataVersion getDataVersion() {
        return dataVersion;
    }

    public void deleteSubscriptionGroupConfig(final String groupName) {
        SubscriptionGroupConfig old = removeSubscriptionGroupConfig(groupName);
        this.forbiddenTable.remove(groupName);
        if (old != null) {
            log.info("delete subscription group OK, subscription group:{}", old);
            long stateMachineVersion = brokerController.getMessageStore() != null ? brokerController.getMessageStore().getStateMachineVersion() : 0;
            dataVersion.nextVersion(stateMachineVersion);
            this.persist();
        } else {
            log.warn("delete subscription group failed, subscription groupName: {} not exist", groupName);
        }
    }


    public void setSubscriptionGroupTable(ConcurrentMap<String, SubscriptionGroupConfig> subscriptionGroupTable) {
        this.subscriptionGroupTable = subscriptionGroupTable;
    }

    public boolean containsSubscriptionGroup(String group) {
        if (StringUtils.isBlank(group)) {
            return false;
        }

        return subscriptionGroupTable.containsKey(group);
    }

    /**
     * 如果消费组订阅配置信息中不包含属性集合，则返回空的HashMap集合，否则返回其属性集合
     * @param subscriptionGroupConfig
     * @return
     */
    private Map<String, String> request(SubscriptionGroupConfig subscriptionGroupConfig) {
        return subscriptionGroupConfig.getAttributes() == null ? new HashMap<>() : subscriptionGroupConfig.getAttributes();
    }

    /**
     * 获取【当前保存的】指定消费组名称的消费组订阅配置信息的属性集合：
     * <ol>
     * <li>从消费组订阅配置信息表中获取指定消费组的订阅配置信息；</li>
     * <li>如果没有找到，就返回空的HashMap集合。</li>
     * <li>如果消费组订阅配置信息找到了，则获取其属性的Map集合</li>
     * <li>如果属性集合为null，则返回空的HashMap集合</li>
     * <li>否则直接返回获取到的属性集合信息</li>
     * </ol>
     * @param groupName
     * @return
     */
    private Map<String, String> current(String groupName) {
        // 从消费组订阅配置信息表中获取指定消费组的订阅配置信息
        SubscriptionGroupConfig subscriptionGroupConfig = this.subscriptionGroupTable.get(groupName);
        // 如果没有找到，就返回空的HashMap集合。
        if (subscriptionGroupConfig == null) {
            return new HashMap<>();
        } else {
            // 如果消费组订阅配置信息找到了，则获取其属性的Map集合
            Map<String, String> attributes = subscriptionGroupConfig.getAttributes();
            // 如果属性集合为null，则返回空的HashMap集合
            if (attributes == null) {
                return new HashMap<>();
            } else {
                // 否则直接返回获取到的属性集合信息
                return attributes;
            }
        }
    }
}
